Skip to main content

Core Classes

Owner: Thilina Deepal

Introduction

The core classes live in api/core/classes/. The framework loads them on every request, so you use them without any require. These are the ones app code works with:

ClassFileWhat it does
ControllerController.class.phpBase class for your app's API controller
RequestRequest.class.phpThe incoming request: controller, action, method, data and files
ResponseResponse.class.phpBuilds the JSON response
databasedb.class.phpPDO wrapper for the MySQL database
SqlitedbSqlitedb.class.phpWrapper for the local SQLite database
system_configsystem_config.class.phpReads and writes the backend config files
SystemSystem.class.phpWrites the error log and the access log
AppApp.class.phpBase class for your app's main class
ModuleModule.class.phpBase class for framework modules

For how a request reaches your controller, see Architecture. The Todo App tutorial uses all of these classes in one app.


How to write a controller

Your app's backend gets one controller, api/apps/myapp/myappController.class.php. The framework builds the class name from the URL: a request to /api/myapp/get_items loads myappController and calls it with the action get_items.

A controller extends Controller and implements two methods:

  • authenticate() decides whether the request may run. Return true or false.
  • Render() handles the request and echoes a response.

The framework calls authenticate() first. When it returns false, the client gets 401 Not Authorized and Render() never runs.

<?php

class myappController extends Controller
{
public function authenticate()
{
$action = $this->getRequest()->getAction();
$controller = $this->getRequest()->getController();

// Actions anyone can call without logging in
$publicEndpoints = array();

return auth::appUserPermission($action, $controller, $publicEndpoints);
}

public function Render()
{
switch ($this->getRequest()->getType()) {
case 'POST':
$this->PostHandler();
break;
case 'GET':
$this->GetHandler();
break;
default:
echo Response::badRequest();
break;
}
}

private function GetHandler()
{
switch ($this->getRequest()->getAction()) {
case 'get_items':
$this->get_items();
break;
default:
$this->notSupported();
break;
}
}

private function PostHandler()
{
switch ($this->getRequest()->getAction()) {
case 'add_item':
$this->add_item();
break;
default:
$this->notSupported();
break;
}
}

private function notSupported()
{
$res = new Response();
echo $res->create(501, 'Not Supported yet.', false);
}

private function get_items()
{
$res = new Response();

try {
$data = $this->getRequest()->getData();
$status = $data['status'] ?? 'open';

$res->setData(myappDAO::getItems($status));
echo $res->create(200, 'Items loaded.', true);
} catch (Exception $e) {
System::errorlog(Loging::log($e->getMessage(), 'myappController:get_items', LOG_WARN));
echo $res->create(200, 'Something went wrong.', false);
}
}

private function add_item()
{
// ...
}
}

This is the same pattern as the helloworld app that ships with the framework. Authentication explains auth::appUserPermission() and public actions.


How to read the request

Inside the controller, $this->getRequest() returns the Request object. For a call like this:

GET https://www.example.com/api/myapp/get_items?page=1&per_page=10
CallReturns
getController()myapp
getAction()get_items
getType()GET
getData()['page' => '1', 'per_page' => '10']

What getData() holds depends on the method:

  • GET: the query string ($_GET).
  • POST with a form or multipart body: the form fields ($_POST).
  • POST with Content-Type: application/json: the decoded JSON body. A body that isn't valid JSON gives null, so check before you index into it.
  • Any other method (PUT, PATCH, DELETE): an array holding the single string Request Type Not Supported By API. Use GET and POST only.
  • Shell runs: the key=value pairs from the command line. See Architecture.
$data = $this->getRequest()->getData() ?? [];
$name = trim($data['name'] ?? '');

Uploaded files come from getFiles(), which returns $_FILES for a POST:

$files = $this->getRequest()->getFiles();
$upload = $files['attachment'] ?? null;

On the command line getType() returns cli, not GET or POST. The Request constructor also stops any web request that didn't arrive over HTTPS with 497 Please Use HTTPS!, and any request whose body was over PHP's post_max_size with 413. PHP drops such a body, so without this the action would report its fields and files as missing.


How to send a response

Every action answers with a Response. Create it, set the data if there is any, then echo the JSON that create() returns:

$res = new Response();
$res->setData($items);
echo $res->create(200, 'Items loaded.', true);

The client receives:

{
"response": {
"statusCode": 200,
"statusMsg": "Items loaded.",
"success": true
},
"data": [
{ "id": 1, "name": "First item" }
]
}
  • create() sets the HTTP status line from the code. A code the framework doesn't know is sent as 999 API Error.
  • data is left out when the data is empty or falsy ([], "", 0, false, null).
  • errors is added when the request logged errors through System::errorlog() and <log_db> is true in the config.

Remember to echo the result: create() only returns the string. For a handled failure, many actions send 200 with success: false, and the frontend checks response.success. See Status codes the framework sends in Architecture.


How to query the database

database wraps PDO for the MySQL database set in <database> of api/config.<environment>.xml. The usual cycle is query() to prepare, bind() for each value, then execute(), resultset() or single().

There are two ways to write a DAO. Create a database object inside each method, as helloworld does:

class myappDAO
{
public static function getItems(string $status): array
{
$db = new database();
$db->query("SELECT * FROM myapp_items WHERE status = :status ORDER BY id DESC");
$db->bind(':status', $status);
return $db->resultset();
}
}

Or extend database and call the methods on $this:

class myappDAO extends database
{
public function getItem(int $id): array|false
{
$this->query("SELECT * FROM myapp_items WHERE id = :id");
$this->bind(':id', $id);
return $this->single();
}
}

$item = (new myappDAO())->getItem(5);

Use the full table name, with your app's prefix (myapp_items). App Manager explains how tables are created from your manifest.

Things to know:

  • Errors throw. The connection uses PDO::ERRMODE_EXCEPTION, so a failed query throws a PDOException rather than returning false. Wrap database code in try/catch.
  • A failed connection doesn't throw. The constructor logs the error and carries on. The first query() then throws Database connection not initialized. getError() returns the PDO message.
  • Every setting is required. The host, database name, user name and password in the config must all be non-empty, so a MySQL user without a password can't connect.
  • One connection per request. Since 0.0.23, every database object with the same credentials shares one PDO handle for the whole request. A transaction opened on one object covers the queries of every other object, including the ones that AppManager helpers create.
  • Timestamps match PHP. Each new connection sets the MySQL session time zone to PHP's, so CURRENT_TIMESTAMP agrees with date().

Use a transaction​

$db = new database();

try {
$db->beginTransaction();

$db->query("INSERT INTO myapp_orders (customer_id) VALUES (:customer_id)");
$db->bind(':customer_id', $customer_id);
$db->execute();
$order_id = $db->lastInsertId();

foreach ($lines as $line) {
$db->query("INSERT INTO myapp_order_lines (order_id, item_id, qty) VALUES (:order_id, :item_id, :qty)");
$db->bind(':order_id', $order_id);
$db->bind(':item_id', $line['item_id']);
$db->bind(':qty', $line['qty']);
$db->execute();
}

$db->endTransaction();
} catch (Exception $e) {
if ($db->inTransaction()) {
$db->cancelTransaction();
}
System::errorlog(Loging::log($e->getMessage(), 'myappDAO:createOrder', LOG_ERROR));
return false;
}

Because the handle is shared, a helper that calls beginTransaction() while its caller already has a transaction open gets a PDOException ("There is already an active transaction"). A helper that might run inside someone else's transaction should only open its own when none is open:

$ownsTransaction = !$db->inTransaction();
if ($ownsTransaction) {
$db->beginTransaction();
}

// ... queries ...

if ($ownsTransaction) {
$db->endTransaction();
}

Choose where the credentials come from​

The constructor takes one flag:

FlagBehaviour
database::ENABLE_SESSION_CREDENTIALS (default)The first database in a PHP session stores the connection details, encrypted, in the session. Every later database in that session uses the stored details. changeConnection() replaces them, which is how one user's session is pointed at a different database.
database::DISABLE_SESSION_CREDENTIALSAlways reads <database> from the config. The session is neither read nor written.
// Always the database from the config, whatever the session holds
$db = new database(database::DISABLE_SESSION_CREDENTIALS);

How to use the local SQLite database

Sqlitedb opens the framework's local SQLite file, api/db/do.db by default (<local_db_log_dir> and <local_db_log_file> in <logs>). The framework keeps its error log there. The API is close to database:

$sqlite = new Sqlitedb();
$sqlite->query("SELECT * FROM error_log WHERE ErrorType = :type ORDER BY Date DESC LIMIT 20");
$sqlite->bind(':type', LOG_CRITICAL);
$rows = $sqlite->resultset();

To open a different file, use Sqlitedb::withPath('/full/path/to/file.db'). The connection closes when the object is destroyed.


How to read and change config

system_config is a static class for the backend config: api/config.xml and the overlay api/config.<environment>.xml.

$timezone = system_config::get('system', 'system_api_timezone');
$status = system_config::get_system_status(); // up, maintenance or down

Config Handlers documents all of its methods, and Configuration Files explains which file holds which setting.


How to log errors

Use System::errorlog() with a Log built by Loging::log():

try {
// ...
} catch (Exception $e) {
System::errorlog(Loging::log($e->getMessage(), 'myappController:add_item', LOG_WARN));
}

The entry goes to the error_log table of the local SQLite database, where admins read it on the admin panel's Logs page. It's also added to the current response's errors key when <log_db> is true, and mailed when mail errors are on for that log type. The log types are LOG_ERROR, LOG_WARN, LOG_NOTICES, LOG_EXCEPTION and LOG_CRITICAL.

To record what a user did, use the access log:

System::acesslog(Loging::AccessLog('Create item', 'create', 'Item created'));

This inserts a row into the access_log table of the MySQL database, with the session id, IP address and device details. See Logging.


How to write the app class

Each app has one main class that extends App, for example api/apps/myapp/myapp.class.php. The framework creates it on every web, shell and heartbeat request, for every active app.

<?php

class myapp extends App
{
public function register(): bool
{
// Phase 1: set up what other apps may need from you.
return true;
}

public function boot(): void
{
// Phase 2: every active app has run register().
}

public function init()
{
// Runs from the constructor. Keep it cheap.
}

public function search(string $search_text)
{
return null;
}
}

The lifecycle, in order:

  1. init() runs from the constructor. It also runs every time another app calls AppManager::CreateAppInstance() for your app.
  2. register() runs on every active app, dependencies first.
  3. boot() runs on every active app after all of them have registered, again dependencies first. It's optional: the base class has an empty one.

An exception in one app's register() or boot() is logged and doesn't stop the other apps. App Manager covers dependency order, namespaced app classes and how the class is found.

search() feeds the search box in the top bar. A POST to /api/system/global_search with search_text calls search() on every app and returns what each one gives back. Return null to stay out of the results, or an array like this:

public function search(string $search_text)
{
$matches = myappDAO::searchItems($search_text);

return [
'appInfo' => [
'label' => 'My App',
'icon' => 'fal fa-box',
'path_name' => 'myapp',
'app_image' => '/apps/myapp/assets/app_icons/app-icon.png',
],
'results' => array_map(fn ($item) => [
'text' => $item['name'],
'type' => 'item',
'path' => '/myapp/items/' . $item['id'],
], $matches),
'searchResults' => !empty($matches),
];
}

The search creates your app through AppManager::CreateAppInstance(), so your manifest's <app_permissions> must allow xp_system. searchResults must be true for the app to be listed.


How modules work

Module is the base class of the framework modules in api/core/Modules/ (Util, Curl, AppManager, Encryption and the rest). You use modules far more often than you write one. See Modules.

A module lives in api/core/Modules/<Name>/<Name>.class.php and implements two methods:

class MyModule extends Module
{
public function init()
{
// Runs from the constructor. Constructor arguments are in $this->params.
}

public function healthcheck()
{
// Return an array of error messages, empty when all is well.
return array();
}
}

The first time a module class is used in a request, the autoloader loads it, creates one instance with no arguments and calls healthcheck(). Any errors are logged as warnings. So the constructor and init() must work without arguments. A module listed in <modules> of the config whose file is missing fails the health check, and every request returns 500.


Methods reference

Controller​

authenticate​

Description:

The authenticate method decides whether the request may run. The framework calls it before Render(). The base version returns SessionManager::validateSession(). Override it.

Syntax:

public function authenticate()
{
return auth::appUserPermission($this->getRequest()->getAction(), $this->getRequest()->getController(), []);
}

Return Value:

  • Boolean: true to run Render(), false to answer 401 Not Authorized (500 on the command line).

Render​

Description:

The Render method handles the request. It's abstract, so every controller must implement it. It returns nothing: echo the response.

Syntax:

public function Render()
{
// switch on getType() and getAction()
}

getRequest​

Description:

The getRequest method returns the Request the controller was created with.

Syntax:

$request = $this->getRequest();

Return Value:

  • Request: The current request.

Request​

getController / getAction​

Description:

getController returns the controller segment of the URL as typed, before lower-casing. getAction returns the action segment.

Syntax:

$controller = $this->getRequest()->getController(); // "myapp"
$action = $this->getRequest()->getAction(); // "get_items"

Return Value:

  • String: The segment.

getType​

Description:

The getType method returns the HTTP method, or cli on the command line.

Syntax:

$type = $this->getRequest()->getType();

Return Value:

  • String: GET, POST, another HTTP method, or cli.

getData​

Description:

The getData method returns the request data: the query string for GET, the form fields or decoded JSON body for POST, the command-line pairs in a shell run.

Syntax:

$data = $this->getRequest()->getData() ?? [];

Return Value:

  • Array: The data.
  • null: A JSON body that couldn't be decoded.

getFiles​

Description:

The getFiles method returns the uploaded files of a POST request, in the $_FILES format. On a GET request or a shell run it throws an Error.

Syntax:

$files = $this->getRequest()->getFiles();

Return Value:

  • Array: The uploaded files. Empty when none were sent.

Response​

create​

Description:

The create method sets the HTTP status line and returns the response as pretty-printed JSON. It doesn't echo it.

Syntax:

$res = new Response();
echo $res->create(200, 'Saved.', true);

Parameters:

  • $statusCode: The HTTP status code.
  • $statusMsg: A human-readable message.
  • $success: true or false, read by the frontend.

Return Value:

  • String: The JSON response.

setData​

Description:

The setData method sets the value sent under data. You can also pass it to the constructor: new Response($items).

Syntax:

$res->setData(['id' => $id]);

Parameters:

  • $data: Any JSON-encodable value. An empty or falsy value is left out of the response.

badRequest​

Description:

The static badRequest method returns a ready-made 400 Bad request response with success: false. It's the only static helper. Controller::defaultHandler() returns the same thing.

Syntax:

echo Response::badRequest();

Return Value:

  • String: The JSON response.

Other Response methods​

getJson() and getOutPutArr() return the last result of create() as a string and as an array. getStatusCode(), getStatusMsg(), getSuccess(), getData() and getResponse() return the parts. Each has a matching setter.


database​

query​

Description:

The query method prepares an SQL statement. Use named placeholders for every value.

Syntax:

$db->query("SELECT * FROM myapp_items WHERE id = :id");

Parameters:

  • $query: The SQL.

bind​

Description:

The bind method binds a value to a placeholder. Without a type, it picks PDO::PARAM_INT, PARAM_BOOL, PARAM_NULL or PARAM_STR from the value. Call it after query().

Syntax:

$db->bind(':id', $id);
$db->bind(':code', $code, PDO::PARAM_STR);

Parameters:

  • $param: The placeholder name, or its position.
  • $value: The value.
  • $type (optional): A PDO::PARAM_* constant.

execute​

Description:

The execute method runs the prepared statement. Use it for INSERT, UPDATE and DELETE. Instead of bind(), you can pass all the values as an array; they're then bound as strings.

Syntax:

$db->execute();
$db->execute([':id' => $id]);

Return Value:

  • Boolean: true on success. A failure throws a PDOException.

resultset​

Description:

The resultset method runs the statement and returns every row as an associative array.

Syntax:

$rows = $db->resultset();

Return Value:

  • Array: The rows. Empty when nothing matched.

single​

Description:

The single method runs the statement and returns the first row.

Syntax:

$row = $db->single();

Return Value:

  • Array: The row.
  • false: No row matched.

rowCount​

Description:

The rowCount method returns the number of rows the last INSERT, UPDATE or DELETE changed.

Syntax:

$changed = $db->rowCount();

Return Value:

  • Integer: The row count.

lastInsertId​

Description:

The lastInsertId method returns the id of the last inserted row on the connection.

Syntax:

$id = $db->lastInsertId();

Return Value:

  • String: The id, as a string.

beginTransaction / endTransaction / cancelTransaction​

Description:

beginTransaction starts a transaction, endTransaction commits it and cancelTransaction rolls it back. The transaction is on the shared connection, so it covers every database object in the request.

Syntax:

$db->beginTransaction();
$db->endTransaction();
$db->cancelTransaction();

Return Value:

  • Boolean: true on success. Starting a second transaction, or committing when none is open, throws a PDOException.

inTransaction​

Description:

The inTransaction method tells you whether a transaction is open on the connection. Added in 0.0.23.

Syntax:

if (!$db->inTransaction()) {
$db->beginTransaction();
}

Return Value:

  • Boolean: true when a transaction is open.

changeConnection​

Description:

The changeConnection method connects this object to a different server or database, and stores the new credentials in the session so later database objects in the same session use them too.

Syntax:

$db->changeConnection($host, $dbname, $username, $password);

Parameters:

  • $host, $dbname, $username, $password: The new connection details. All are required.

Return Value:

  • Boolean: true on success, false on failure. getError() has the reason.

resetConnection​

Description:

The resetConnection method reconnects with the <database> settings from the config. With session credentials enabled, it also writes them back into the session.

Syntax:

$db->resetConnection();

Return Value:

  • Boolean: true on success.

selectDatabase​

Description:

The selectDatabase method runs USE on the connection. The connection is shared, so every database object in the request switches with it.

Syntax:

$db->selectDatabase('other_db');

Parameters:

  • $dbname: Letters, digits, _ and $ only.

Return Value:

  • Boolean: true on success.

Other database methods​

  • getError(): the last connection error message, or null.
  • nextRowsets(): steps through the results of a multi-statement query, so an error in a later statement is raised.
  • debugDumpParams(): prints the prepared statement and its bound values.
  • getDatabaseName(), getDatabaseUsername(), getDatabaseHost(): the current connection details.

Sqlitedb​

query / bind / execute / resultset​

Description:

These work like their database counterparts. bind() picks SQLITE3_INTEGER, SQLITE3_NULL or SQLITE3_TEXT from the value. execute() returns an SQLite3Result, or false on failure. resultset() returns every row as an associative array.

Syntax:

$sqlite = new Sqlitedb();
$sqlite->query("SELECT * FROM error_log WHERE ClassName = :class");
$sqlite->bind(':class', 'myappController:add_item');
$rows = $sqlite->resultset();

Other Sqlitedb methods​

  • Sqlitedb::withPath($path): opens the SQLite file at $path instead of the default one.
  • exec($sql): runs SQL with no result set, such as CREATE TABLE. Returns true or false.
  • querySingleRow($sql): the first row of a query, as an array.
  • querySingleField($sql): the first column of the first row.
  • getLastInsertRowID(): the row id of the last insert.
  • getLastErrorCode(), getLastErrorMsg(): the last SQLite error.
  • checkIfTablesExists(): the names of all tables, as rows.
  • Sqlitedb::getEscapedString($string): escapes a string for use in SQL. Prefer bind().

System​

errorlog​

Description:

The static errorlog method saves a log entry in the local SQLite database and adds it to the current response's errors. It mails the entry when mail errors are on for its type.

Syntax:

System::errorlog(Loging::log($e->getMessage(), 'myappController:add_item', LOG_WARN));

Parameters:

  • $log: A Log from Loging::log($text, $className, $type).

Return Value:

  • Boolean: true.

acesslog​

Description:

The static acesslog method (spelled with one "c") saves a user action in the access_log table of the MySQL database.

Syntax:

System::acesslog(Loging::AccessLog('Create item', 'create', 'Item created'));

Parameters:

  • $log: An AccessLog from Loging::AccessLog($action, $actionType, $message).

Return Value:

  • Boolean: Always false, even when the row was saved, so don't test it.

log​

Description:

The static log method is deprecated. Use errorlog().


App​

register​

Description:

The abstract register method is the first boot phase. It runs on every active app, dependencies first, on every request.

Syntax:

public function register(): bool
{
return true;
}

Return Value:

  • Boolean: Return true. The framework doesn't use the value.

boot​

Description:

The optional boot method is the second boot phase. It runs after every active app has run register(), so it can use what other apps set up there.

Syntax:

public function boot(): void
{
}

init​

Description:

The abstract init method runs from the constructor, every time the app class is created.

Syntax:

public function init()
{
}

Description:

The abstract search method answers the global search. See Support global search.

Syntax:

public function search(string $search_text)
{
return null;
}

Parameters:

  • $search_text: What the user typed.

Return Value:

  • Array: appInfo, results and searchResults.
  • null: No results from this app.

Module​

init​

Description:

The abstract init method runs from the constructor. Arguments passed to the constructor are in $this->params.

Syntax:

public function init()
{
}

healthcheck​

Description:

The abstract healthcheck method runs when the module is first loaded in a request.

Syntax:

public function healthcheck()
{
return array();
}

Return Value:

  • Array: Error messages to log as warnings. Empty when all is well.